import { EventDispatcher, MOUSE, Quaternion, Vector2, Vector3 } from './three.module.js'

const _changeEvent = { type: 'change' }
const _startEvent = { type: 'start' }
const _endEvent = { type: 'end' }

class TrackballControls extends EventDispatcher {
  constructor(object, domElement) {
    super()

    if (domElement === undefined) console.warn('THREE.TrackballControls: The second parameter "domElement" is now mandatory.')
    if (domElement === document)
      console.error(
        'THREE.TrackballControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.'
      )

    const scope = this
    const STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 }

    this.object = object
    this.domElement = domElement
    this.domElement.style.touchAction = 'none' // disable touch scroll

    // API

    this.enabled = true

    this.screen = { left: 0, top: 0, width: 0, height: 0 }

    this.rotateSpeed = 1.0
    this.zoomSpeed = 1.2
    this.panSpeed = 0.3

    this.noRotate = false
    this.noZoom = false
    this.noPan = false

    this.staticMoving = false
    this.dynamicDampingFactor = 0.2

    this.minDistance = 0
    this.maxDistance = Infinity

    this.keys = ['KeyA' /*A*/, 'KeyS' /*S*/, 'KeyD' /*D*/]

    this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }

    // internals

    this.target = new Vector3()

    const EPS = 0.000001

    const lastPosition = new Vector3()
    let lastZoom = 1

    let _state = STATE.NONE,
      _keyState = STATE.NONE,
      _touchZoomDistanceStart = 0,
      _touchZoomDistanceEnd = 0,
      _lastAngle = 0

    const _eye = new Vector3(),
      _movePrev = new Vector2(),
      _moveCurr = new Vector2(),
      _lastAxis = new Vector3(),
      _zoomStart = new Vector2(),
      _zoomEnd = new Vector2(),
      _panStart = new Vector2(),
      _panEnd = new Vector2(),
      _pointers = [],
      _pointerPositions = {}

    // for reset

    this.target0 = this.target.clone()
    this.position0 = this.object.position.clone()
    this.up0 = this.object.up.clone()
    this.zoom0 = this.object.zoom

    // methods

    this.handleResize = function() {
      const box = scope.domElement.getBoundingClientRect()
      // adjustments come from similar code in the jquery offset() function
      const d = scope.domElement.ownerDocument.documentElement
      scope.screen.left = box.left + window.pageXOffset - d.clientLeft
      scope.screen.top = box.top + window.pageYOffset - d.clientTop
      scope.screen.width = box.width
      scope.screen.height = box.height
    }

    const getMouseOnScreen = (function() {
      const vector = new Vector2()

      return function getMouseOnScreen(pageX, pageY) {
        vector.set((pageX - scope.screen.left) / scope.screen.width, (pageY - scope.screen.top) / scope.screen.height)

        return vector
      }
    })()

    const getMouseOnCircle = (function() {
      const vector = new Vector2()

      return function getMouseOnCircle(pageX, pageY) {
        vector.set(
          (pageX - scope.screen.width * 0.5 - scope.screen.left) / (scope.screen.width * 0.5),
          (scope.screen.height + 2 * (scope.screen.top - pageY)) / scope.screen.width // screen.width intentional
        )

        return vector
      }
    })()

    this.rotateCamera = (function() {
      const axis = new Vector3(),
        quaternion = new Quaternion(),
        eyeDirection = new Vector3(),
        objectUpDirection = new Vector3(),
        objectSidewaysDirection = new Vector3(),
        moveDirection = new Vector3()

      return function rotateCamera() {
        moveDirection.set(_moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0)
        let angle = moveDirection.length()

        if (angle) {
          _eye.copy(scope.object.position).sub(scope.target)

          eyeDirection.copy(_eye).normalize()
          objectUpDirection.copy(scope.object.up).normalize()
          objectSidewaysDirection.crossVectors(objectUpDirection, eyeDirection).normalize()

          objectUpDirection.setLength(_moveCurr.y - _movePrev.y)
          objectSidewaysDirection.setLength(_moveCurr.x - _movePrev.x)

          moveDirection.copy(objectUpDirection.add(objectSidewaysDirection))

          axis.crossVectors(moveDirection, _eye).normalize()

          angle *= scope.rotateSpeed
          quaternion.setFromAxisAngle(axis, angle)

          _eye.applyQuaternion(quaternion)
          scope.object.up.applyQuaternion(quaternion)

          _lastAxis.copy(axis)
          _lastAngle = angle
        } else if (!scope.staticMoving && _lastAngle) {
          _lastAngle *= Math.sqrt(1.0 - scope.dynamicDampingFactor)
          _eye.copy(scope.object.position).sub(scope.target)
          quaternion.setFromAxisAngle(_lastAxis, _lastAngle)
          _eye.applyQuaternion(quaternion)
          scope.object.up.applyQuaternion(quaternion)
        }

        _movePrev.copy(_moveCurr)
      }
    })()

    this.zoomCamera = function() {
      let factor

      if (_state === STATE.TOUCH_ZOOM_PAN) {
        factor = _touchZoomDistanceStart / _touchZoomDistanceEnd
        _touchZoomDistanceStart = _touchZoomDistanceEnd

        if (scope.object.isPerspectiveCamera) {
          _eye.multiplyScalar(factor)
        } else if (scope.object.isOrthographicCamera) {
          scope.object.zoom *= factor
          scope.object.updateProjectionMatrix()
        } else {
          console.warn('THREE.TrackballControls: Unsupported camera type')
        }
      } else {
        factor = 1.0 + (_zoomEnd.y - _zoomStart.y) * scope.zoomSpeed

        if (factor !== 1.0 && factor > 0.0) {
          if (scope.object.isPerspectiveCamera) {
            _eye.multiplyScalar(factor)
          } else if (scope.object.isOrthographicCamera) {
            scope.object.zoom /= factor
            scope.object.updateProjectionMatrix()
          } else {
            console.warn('THREE.TrackballControls: Unsupported camera type')
          }
        }

        if (scope.staticMoving) {
          _zoomStart.copy(_zoomEnd)
        } else {
          _zoomStart.y += (_zoomEnd.y - _zoomStart.y) * this.dynamicDampingFactor
        }
      }
    }

    this.panCamera = (function() {
      const mouseChange = new Vector2(),
        objectUp = new Vector3(),
        pan = new Vector3()

      return function panCamera() {
        mouseChange.copy(_panEnd).sub(_panStart)

        if (mouseChange.lengthSq()) {
          if (scope.object.isOrthographicCamera) {
            const scale_x = (scope.object.right - scope.object.left) / scope.object.zoom / scope.domElement.clientWidth
            const scale_y = (scope.object.top - scope.object.bottom) / scope.object.zoom / scope.domElement.clientWidth

            mouseChange.x *= scale_x
            mouseChange.y *= scale_y
          }

          mouseChange.multiplyScalar(_eye.length() * scope.panSpeed)

          pan
            .copy(_eye)
            .cross(scope.object.up)
            .setLength(mouseChange.x)
          pan.add(objectUp.copy(scope.object.up).setLength(mouseChange.y))

          scope.object.position.add(pan)
          scope.target.add(pan)

          if (scope.staticMoving) {
            _panStart.copy(_panEnd)
          } else {
            _panStart.add(mouseChange.subVectors(_panEnd, _panStart).multiplyScalar(scope.dynamicDampingFactor))
          }
        }
      }
    })()

    this.checkDistances = function() {
      if (!scope.noZoom || !scope.noPan) {
        if (_eye.lengthSq() > scope.maxDistance * scope.maxDistance) {
          scope.object.position.addVectors(scope.target, _eye.setLength(scope.maxDistance))
          _zoomStart.copy(_zoomEnd)
        }

        if (_eye.lengthSq() < scope.minDistance * scope.minDistance) {
          scope.object.position.addVectors(scope.target, _eye.setLength(scope.minDistance))
          _zoomStart.copy(_zoomEnd)
        }
      }
    }

    this.update = function() {
      _eye.subVectors(scope.object.position, scope.target)

      if (!scope.noRotate) {
        scope.rotateCamera()
      }

      if (!scope.noZoom) {
        scope.zoomCamera()
      }

      if (!scope.noPan) {
        scope.panCamera()
      }

      scope.object.position.addVectors(scope.target, _eye)

      if (scope.object.isPerspectiveCamera) {
        scope.checkDistances()

        scope.object.lookAt(scope.target)

        if (lastPosition.distanceToSquared(scope.object.position) > EPS) {
          scope.dispatchEvent(_changeEvent)

          lastPosition.copy(scope.object.position)
        }
      } else if (scope.object.isOrthographicCamera) {
        scope.object.lookAt(scope.target)

        if (lastPosition.distanceToSquared(scope.object.position) > EPS || lastZoom !== scope.object.zoom) {
          scope.dispatchEvent(_changeEvent)

          lastPosition.copy(scope.object.position)
          lastZoom = scope.object.zoom
        }
      } else {
        console.warn('THREE.TrackballControls: Unsupported camera type')
      }
    }

    this.reset = function() {
      _state = STATE.NONE
      _keyState = STATE.NONE

      scope.target.copy(scope.target0)
      scope.object.position.copy(scope.position0)
      scope.object.up.copy(scope.up0)
      scope.object.zoom = scope.zoom0

      scope.object.updateProjectionMatrix()

      _eye.subVectors(scope.object.position, scope.target)

      scope.object.lookAt(scope.target)

      scope.dispatchEvent(_changeEvent)

      lastPosition.copy(scope.object.position)
      lastZoom = scope.object.zoom
    }

    // listeners

    function onPointerDown(event) {
      if (scope.enabled === false) return

      if (_pointers.length === 0) {
        scope.domElement.ownerDocument.addEventListener('pointermove', onPointerMove)
        scope.domElement.ownerDocument.addEventListener('pointerup', onPointerUp)
      }

      //

      addPointer(event)

      if (event.pointerType === 'touch') {
        onTouchStart(event)
      } else {
        onMouseDown(event)
      }
    }

    function onPointerMove(event) {
      if (scope.enabled === false) return

      if (event.pointerType === 'touch') {
        onTouchMove(event)
      } else {
        onMouseMove(event)
      }
    }

    function onPointerUp(event) {
      if (scope.enabled === false) return

      if (event.pointerType === 'touch') {
        onTouchEnd(event)
      } else {
        onMouseUp()
      }

      //

      removePointer(event)

      if (_pointers.length === 0) {
        scope.domElement.ownerDocument.removeEventListener('pointermove', onPointerMove)
        scope.domElement.ownerDocument.removeEventListener('pointerup', onPointerUp)
      }
    }

    function onPointerCancel(event) {
      removePointer(event)
    }

    function keydown(event) {
      if (scope.enabled === false) return

      window.removeEventListener('keydown', keydown)

      if (_keyState !== STATE.NONE) {
        return
      } else if (event.code === scope.keys[STATE.ROTATE] && !scope.noRotate) {
        _keyState = STATE.ROTATE
      } else if (event.code === scope.keys[STATE.ZOOM] && !scope.noZoom) {
        _keyState = STATE.ZOOM
      } else if (event.code === scope.keys[STATE.PAN] && !scope.noPan) {
        _keyState = STATE.PAN
      }
    }

    function keyup() {
      if (scope.enabled === false) return

      _keyState = STATE.NONE

      window.addEventListener('keydown', keydown)
    }

    function onMouseDown(event) {
      if (_state === STATE.NONE) {
        switch (event.button) {
          case scope.mouseButtons.LEFT:
            _state = STATE.ROTATE
            break

          case scope.mouseButtons.MIDDLE:
            _state = STATE.ZOOM
            break

          case scope.mouseButtons.RIGHT:
            _state = STATE.PAN
            break

          default:
            _state = STATE.NONE
        }
      }

      const state = _keyState !== STATE.NONE ? _keyState : _state

      if (state === STATE.ROTATE && !scope.noRotate) {
        _moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY))
        _movePrev.copy(_moveCurr)
      } else if (state === STATE.ZOOM && !scope.noZoom) {
        _zoomStart.copy(getMouseOnScreen(event.pageX, event.pageY))
        _zoomEnd.copy(_zoomStart)
      } else if (state === STATE.PAN && !scope.noPan) {
        _panStart.copy(getMouseOnScreen(event.pageX, event.pageY))
        _panEnd.copy(_panStart)
      }

      scope.domElement.ownerDocument.addEventListener('pointermove', onPointerMove)
      scope.domElement.ownerDocument.addEventListener('pointerup', onPointerUp)

      scope.dispatchEvent(_startEvent)
    }

    function onMouseMove(event) {
      const state = _keyState !== STATE.NONE ? _keyState : _state

      if (state === STATE.ROTATE && !scope.noRotate) {
        _movePrev.copy(_moveCurr)
        _moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY))
      } else if (state === STATE.ZOOM && !scope.noZoom) {
        _zoomEnd.copy(getMouseOnScreen(event.pageX, event.pageY))
      } else if (state === STATE.PAN && !scope.noPan) {
        _panEnd.copy(getMouseOnScreen(event.pageX, event.pageY))
      }
    }

    function onMouseUp() {
      _state = STATE.NONE

      scope.domElement.ownerDocument.removeEventListener('pointermove', onPointerMove)
      scope.domElement.ownerDocument.removeEventListener('pointerup', onPointerUp)

      scope.dispatchEvent(_endEvent)
    }

    function onMouseWheel(event) {
      if (scope.enabled === false) return

      if (scope.noZoom === true) return

      event.preventDefault()

      switch (event.deltaMode) {
        case 2:
          // Zoom in pages
          _zoomStart.y -= event.deltaY * 0.025
          break

        case 1:
          // Zoom in lines
          _zoomStart.y -= event.deltaY * 0.01
          break

        default:
          // undefined, 0, assume pixels
          _zoomStart.y -= event.deltaY * 0.00025
          break
      }

      scope.dispatchEvent(_startEvent)
      scope.dispatchEvent(_endEvent)
    }

    function onTouchStart(event) {
      trackPointer(event)

      switch (_pointers.length) {
        case 1:
          _state = STATE.TOUCH_ROTATE
          _moveCurr.copy(getMouseOnCircle(_pointers[0].pageX, _pointers[0].pageY))
          _movePrev.copy(_moveCurr)
          break

        default:
          // 2 or more
          _state = STATE.TOUCH_ZOOM_PAN
          const dx = _pointers[0].pageX - _pointers[1].pageX
          const dy = _pointers[0].pageY - _pointers[1].pageY
          _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt(dx * dx + dy * dy)

          const x = (_pointers[0].pageX + _pointers[1].pageX) / 2
          const y = (_pointers[0].pageY + _pointers[1].pageY) / 2
          _panStart.copy(getMouseOnScreen(x, y))
          _panEnd.copy(_panStart)
          break
      }

      scope.dispatchEvent(_startEvent)
    }

    function onTouchMove(event) {
      trackPointer(event)

      switch (_pointers.length) {
        case 1:
          _movePrev.copy(_moveCurr)
          _moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY))
          break

        default:
          // 2 or more

          const position = getSecondPointerPosition(event)

          const dx = event.pageX - position.x
          const dy = event.pageY - position.y
          _touchZoomDistanceEnd = Math.sqrt(dx * dx + dy * dy)

          const x = (event.pageX + position.x) / 2
          const y = (event.pageY + position.y) / 2
          _panEnd.copy(getMouseOnScreen(x, y))
          break
      }
    }

    function onTouchEnd(event) {
      switch (_pointers.length) {
        case 0:
          _state = STATE.NONE
          break

        case 1:
          _state = STATE.TOUCH_ROTATE
          _moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY))
          _movePrev.copy(_moveCurr)
          break
      }

      scope.dispatchEvent(_endEvent)
    }

    function contextmenu(event) {
      if (scope.enabled === false) return

      event.preventDefault()
    }

    function addPointer(event) {
      _pointers.push(event)
    }

    function removePointer(event) {
      delete _pointerPositions[event.pointerId]

      for (let i = 0; i < _pointers.length; i++) {
        if (_pointers[i].pointerId == event.pointerId) {
          _pointers.splice(i, 1)
          return
        }
      }
    }

    function trackPointer(event) {
      let position = _pointerPositions[event.pointerId]

      if (position === undefined) {
        position = new Vector2()
        _pointerPositions[event.pointerId] = position
      }

      position.set(event.pageX, event.pageY)
    }

    function getSecondPointerPosition(event) {
      const pointer = event.pointerId === _pointers[0].pointerId ? _pointers[1] : _pointers[0]

      return _pointerPositions[pointer.pointerId]
    }

    this.dispose = function() {
      scope.domElement.removeEventListener('contextmenu', contextmenu)

      scope.domElement.removeEventListener('pointerdown', onPointerDown)
      scope.domElement.removeEventListener('pointercancel', onPointerCancel)
      scope.domElement.removeEventListener('wheel', onMouseWheel)

      window.removeEventListener('keydown', keydown)
      window.removeEventListener('keyup', keyup)
    }

    this.domElement.addEventListener('contextmenu', contextmenu)

    this.domElement.addEventListener('pointerdown', onPointerDown)
    this.domElement.addEventListener('pointercancel', onPointerCancel)
    this.domElement.addEventListener('wheel', onMouseWheel, { passive: false })

    window.addEventListener('keydown', keydown)
    window.addEventListener('keyup', keyup)

    this.handleResize()

    // force an update at start
    this.update()
  }
}

export { TrackballControls }
